home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2009 February / PCWFEB09.iso / Software / Linux / Kubuntu 8.10 / kubuntu-8.10-desktop-i386.iso / casper / filesystem.squashfs / usr / share / hplip / base / vcard.py < prev   
Text File  |  2008-10-13  |  45KB  |  1,432 lines

  1. # -*- coding: utf-8 -*-
  2. #
  3. # (c) Copyright 2001-2008 Hewlett-Packard Development Company, L.P.
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
  18. #
  19. # ****************************************************************************
  20. #
  21. # Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com>
  22. #
  23. # This program is free software; you can redistribute it and/or modify
  24. # it under the terms of the BitPim license as detailed in the LICENSE file.
  25. #
  26. # Code for reading and writing Vcard
  27. # VCARD is defined in RFC 2425 and 2426
  28. # Original author: Roger Binns <rogerb@rogerbinns.com>
  29. # Modified for HPLIP by: Don Welch
  30. #
  31.  
  32. # Local
  33. from base.g import *
  34.  
  35. # Std Lib
  36. import quopri
  37. import base64
  38. import codecs
  39. import cStringIO
  40. import re
  41. import StringIO
  42. import codecs
  43.  
  44.  
  45.  
  46. _boms = []
  47. # 64 bit 
  48. try:
  49.      import encodings.utf_64
  50.      _boms.append( (codecs.BOM64_BE, "utf_64") )
  51.      _boms.append( (codecs.BOM64_LE, "utf_64") )
  52. except:  pass
  53.  
  54. # 32 bit
  55. try:
  56.      import encodings.utf_32
  57.      _boms.append( (codecs.BOM_UTF32, "utf_32") )
  58.      _boms.append( (codecs.BOM_UTF32_BE, "utf_32") )
  59.      _boms.append( (codecs.BOM_UTF32_LE, "utf_32") )
  60. except:  pass
  61.  
  62. # 16 bit
  63. try:
  64.      import encodings.utf_16
  65.      _boms.append( (codecs.BOM_UTF16, "utf_16") )
  66.      _boms.append( (codecs.BOM_UTF16_BE, "utf_16_be") )
  67.      _boms.append( (codecs.BOM_UTF16_LE, "utf_16_le") )
  68. except:  pass
  69.  
  70. # 8 bit
  71. try:
  72.      import encodings.utf_8
  73.      _boms.append( (codecs.BOM_UTF8, "utf_8") )
  74. except: pass
  75.  
  76. # Work arounds for Apple
  77. _boms.append( ("\0B\0E\0G\0I\0N\0:\0V\0C\0A\0R\0D", "utf_16_be") )
  78. _boms.append( ("B\0E\0G\0I\0N\0:\0V\0C\0A\0R\0D\0", "utf_16_le") )
  79.  
  80.  
  81. # NB: the 32 bit and 64 bit versions have the BOM constants defined in Py 2.3
  82. # but no corresponding encodings module.  They are here for completeness.
  83. # The order of above also matters since the first ones have longer
  84. # boms than the latter ones, and we need to be unambiguous
  85.  
  86. _maxbomlen = max([len(bom) for bom,codec in _boms])
  87.  
  88. def opentextfile(name):
  89.     """This function detects unicode byte order markers and if present
  90.     uses the codecs module instead to open the file instead with
  91.     appropriate unicode decoding, else returns the file using standard
  92.     open function"""
  93.     #with file(name, 'rb') as f:
  94.     f = file(name, 'rb')
  95.     start = f.read(_maxbomlen)
  96.     for bom,codec in _boms:
  97.         if start.startswith(bom):
  98.             # some codecs don't do readline, so we have to vector via stringio
  99.             # many postings also claim that the BOM is returned as the first
  100.             # character but that hasn't been the case in my testing
  101.             return StringIO.StringIO(codecs.open(name, "r", codec).read())
  102.     return file(name, "rtU")
  103.  
  104.  
  105. _notdigits = re.compile("[^0-9]*")
  106. _tendigits = re.compile("^[0-9]{10}$")
  107. _sevendigits = re.compile("^[0-9]{7}$")
  108.  
  109.  
  110. def phonenumber_normalise(n):
  111.     # this was meant to remove the long distance '1' prefix,
  112.     # temporary disable it, will be done on a phone-by-phone case.
  113.     return n
  114.     nums = "".join(re.split(_notdigits, n))
  115.     if len(nums) == 10:
  116.         return nums
  117.         
  118.     if len(nums) == 11 and nums[0] == "1":
  119.         return nums[1:]
  120.         
  121.     return n
  122.  
  123. def phonenumber_format(n):
  124.     if re.match(_tendigits, n) is not None:
  125.         return "(%s) %s-%s" % (n[0:3], n[3:6], n[6:])
  126.     elif re.match(_sevendigits, n) is not None:
  127.         return "%s-%s" %(n[:3], n[3:])
  128.     return n
  129.  
  130.  
  131. def nameparser_formatsimplename(name):
  132.     "like L{formatname}, except we use the first matching component"
  133.     _fullname = nameparser_getfullname(name)
  134.     if _fullname:
  135.         return _fullname
  136.     return name.get('nickname', "")
  137.  
  138.  
  139. def nameparser_getfullname(name):
  140.     """Gets the full name, joining the first/middle/last if necessary"""
  141.     if name.has_key("full"):
  142.         return name["full"]
  143.     return ' '.join([x for x in nameparser_getparts(name) if x])
  144.  
  145.     
  146. # See the following references for name parsing and how little fun it
  147. # is.
  148. #
  149. # The simple way:
  150. # http://cvs.gnome.org/lxr/source/evolution-data-server/addressbook/libebook/
  151. # e-name-western*
  152. #
  153. # The "proper" way:
  154. # http://cvs.xemacs.org/viewcvs.cgi/XEmacs/packages/xemacs-packages/mail-lib/mail-extr.el
  155. #
  156. # How we do it
  157. #
  158. #  [1] The name is split into white-space seperated parts
  159. #  [2] If there is only one part, it becomes the firstname
  160. #  [3] If there are only two parts, they become first name and surname
  161. #  [4] For three or more parts, the first part is the first name and the last
  162. #      part is the surname.  Then while the last part of the remainder starts with
  163. #      a lower case letter or is in the list below, it is prepended to the surname.
  164. #      Whatever is left becomes the middle name.
  165.  
  166. lastparts = [ "van", "von", "de", "di" ]
  167.  
  168. # I would also like to proudly point out that this code has no comment saying
  169. # "Have I no shame".  It will be considered incomplete until that happens
  170.  
  171. def nameparser_getparts_FML(name):
  172.     n = name.get("full")
  173.     
  174.     # [1]
  175.     parts = n.split()
  176.  
  177.     # [2]
  178.     if len(parts) <= 1:
  179.         return (n, "", "")
  180.     
  181.     # [3]
  182.     if len(parts) == 2:
  183.         return (parts[0], "", parts[1])
  184.  
  185.     # [4]
  186.     f = [parts[0]]
  187.     m = []
  188.     l = [parts[-1]]
  189.     del parts[0]
  190.     del parts[-1]
  191.     
  192.     while len(parts) and (parts[-1][0].lower() == parts[-1][0] or parts[-1].lower() in lastparts):
  193.         l = [parts[-1]]+l
  194.         del parts[-1]
  195.     
  196.     m = parts
  197.  
  198.     # return it all
  199.     return (" ".join(f), " ".join(m), " ".join(l))
  200.  
  201.     
  202. def nameparser_getparts_LFM(name):
  203.     n = name.get("full")
  204.     
  205.     parts = n.split(',')
  206.  
  207.     if len(parts) <= 1:
  208.         return (n, '', '')
  209.     
  210.     _last = parts[0]
  211.     _first = ''
  212.     _middle = ''
  213.     parts = parts[1].split()
  214.     
  215.     if len(parts) >= 1:
  216.         _first = parts[0]
  217.         
  218.         if len(parts) > 1:
  219.             _middle = ' '.join(parts[1:])
  220.             
  221.     return (_first, _middle, _last)
  222.     
  223.     
  224. def nameparser_getparts(name):
  225.     """Returns (first, middle, last) for name.  If the part doesn't exist
  226.     then a blank string is returned"""
  227.  
  228.     # do we have any of the parts?
  229.     for i in ("first", "middle", "last"):
  230.         if name.has_key(i):
  231.             return (name.get("first", ""), name.get("middle", ""), name.get("last", ""))
  232.  
  233.     # check we have full.  if not return nickname
  234.     if not name.has_key("full"):
  235.         return (name.get("nickname", ""), "", "")
  236.  
  237.     n = name.nameparser_get("full")
  238.     
  239.     if ',' in n:
  240.         return nameparser_getparts_LFM(name)
  241.     
  242.     return nameparser_getparts_FML(name)
  243.  
  244.  
  245.  
  246.  
  247. class VFileException(Exception):
  248.     pass
  249.  
  250.     
  251.     
  252. class VFile:
  253.     _charset_aliases = {
  254.         'MACINTOSH': 'MAC_ROMAN'
  255.         }
  256.         
  257.     def __init__(self, source):
  258.         self.source = source    
  259.         self.saved = None
  260.  
  261.         
  262.     def __iter__(self):
  263.         return self
  264.  
  265.         
  266.     def next(self):
  267.         # Get the next non-blank line
  268.         while True:  # python desperately needs do-while
  269.             line = self._getnextline()
  270.             
  271.             if line is None:
  272.                 raise StopIteration()
  273.             
  274.             if len(line) != 0:
  275.                 break
  276.  
  277.         # Hack for evolution.  If ENCODING is QUOTED-PRINTABLE then it doesn't
  278.         # offset the next line, so we look to see what the first char is
  279.         normalcontinuations = True
  280.         colon = line.find(':')
  281.         if colon > 0:
  282.             s = line[:colon].lower().split(";")
  283.             
  284.             if "quoted-printable" in s or 'encoding=quoted-printable' in s:
  285.                 normalcontinuations = False
  286.                 while line[-1] == "=" or line[-2] == '=':
  287.                     if line[-1] == '=':
  288.                         i = -1
  289.                     else:
  290.                         i = -2
  291.                     
  292.                     nextl = self._getnextline()
  293.                     if nextl[0] in ("\t", " "): nextl = nextl[1:]
  294.                     line = line[:i]+nextl
  295.  
  296.         while normalcontinuations:
  297.             nextline = self._lookahead()
  298.             
  299.             if nextline is None:
  300.                 break
  301.             
  302.             if len(nextline) == 0:
  303.                 break
  304.             
  305.             if  nextline[0] != ' ' and nextline[0] != '\t':
  306.                 break
  307.             
  308.             line += self._getnextline()[1:]
  309.  
  310.         colon = line.find(':')
  311.         
  312.         if colon < 1:
  313.             # some evolution vcards don't even have colons
  314.             # raise VFileException("Invalid property: "+line)
  315.             log.debug("Fixing up bad line: %s" % line)
  316.             
  317.             colon = len(line)
  318.             line += ":"
  319.  
  320.         b4 = line[:colon]
  321.         line = line[colon+1:].strip()
  322.  
  323.         # upper case and split on semicolons
  324.         items = b4.upper().split(";")
  325.  
  326.         newitems = []
  327.         if isinstance(line, unicode):
  328.             charset = None
  329.             
  330.         else:
  331.             charset = "LATIN-1"
  332.         
  333.         for i in items:
  334.             # ::TODO:: probably delete anything preceding a '.'
  335.             # (see 5.8.2 in rfc 2425)
  336.             # look for charset parameter
  337.             if i.startswith("CHARSET="):
  338.                 charset = i[8:] or "LATIN-1"
  339.                 continue
  340.            
  341.            # unencode anything that needs it
  342.             if not i.startswith("ENCODING=") and not i=="QUOTED-PRINTABLE": # evolution doesn't bother with "ENCODING="
  343.                 # ::TODO:: deal with backslashes, being especially careful with ones quoting semicolons
  344.                 newitems.append(i)
  345.                 continue
  346.             
  347.             try:
  348.                 if i == 'QUOTED-PRINTABLE' or i == "ENCODING=QUOTED-PRINTABLE":
  349.                     # technically quoted printable is ascii only but we decode anyway since not all vcards comply
  350.                     line = quopri.decodestring(line)
  351.                 
  352.                 elif i == 'ENCODING=B':
  353.                     line = base64.decodestring(line)
  354.                     charset = None
  355.                 
  356.                 else:
  357.                     raise VFileException("unknown encoding: "+i)
  358.                     
  359.             except Exception,e:
  360.                 if isinstance(e,VFileException):
  361.                     raise e
  362.                 raise VFileException("Exception %s while processing encoding %s on data '%s'" % (str(e), i, line))
  363.         
  364.         # ::TODO:: repeat above shenanigans looking for a VALUE= thingy and
  365.         # convert line as in 5.8.4 of rfc 2425
  366.         if len(newitems) == 0:
  367.             raise VFileException("Line contains no property: %s" % (line,))
  368.         
  369.         # charset frigging
  370.         if charset is not None:
  371.             try:
  372.                 decoder = codecs.getdecoder(self._charset_aliases.get(charset, charset))
  373.                 line,_ = decoder(line)
  374.             except LookupError:
  375.                 raise VFileException("unknown character set '%s' in parameters %s" % (charset, b4))          
  376.         
  377.         if newitems == ["BEGIN"] or newitems == ["END"]:
  378.             line = line.upper()
  379.         
  380.         return newitems, line
  381.  
  382.         
  383.     def _getnextline(self):
  384.         if self.saved is not None:
  385.             line = self.saved
  386.             self.saved = None
  387.             return line
  388.         else:
  389.             return self._readandstripline()
  390.  
  391.             
  392.     def _readandstripline(self):
  393.         line = self.source.readline()
  394.         if line is not None:
  395.             if len(line) == 0:
  396.                 return None
  397.                 
  398.             elif line[-2:] == "\r\n":    
  399.                 return line[:-2]
  400.                 
  401.             elif line[-1] == '\r' or line[-1] == '\n':
  402.                 return line[:-1]
  403.                 
  404.         return line
  405.     
  406.     
  407.     def _lookahead(self):
  408.         assert self.saved is None
  409.         self.saved = self._readandstripline()
  410.         return self.saved
  411.         
  412.         
  413.         
  414. class VCards:
  415.     "Understands vcards in a vfile"
  416.  
  417.     
  418.     def __init__(self, vfile):
  419.         self.vfile = vfile
  420.  
  421.         
  422.     def __iter__(self):
  423.         return self
  424.  
  425.         
  426.     def next(self):
  427.         # find vcard start
  428.         field = value = None
  429.         for field,value in self.vfile:
  430.             if (field,value) != (["BEGIN"], "VCARD"):
  431.                 continue
  432.                 
  433.             found = True
  434.             break
  435.         
  436.         if (field,value) != (["BEGIN"], "VCARD"):
  437.             # hit eof without any BEGIN:vcard
  438.             raise StopIteration()
  439.         
  440.         # suck up lines
  441.         lines = []
  442.         for field,value in self.vfile:
  443.             if (field,value) != (["END"], "VCARD"):
  444.                 lines.append( (field,value) )
  445.                 continue
  446.                 
  447.             break
  448.         
  449.         if (field,value) != (["END"], "VCARD"):
  450.             raise VFileException("There is a BEGIN:VCARD but no END:VCARD")
  451.         
  452.         return VCard(lines)
  453.  
  454.         
  455.         
  456. class VCard:
  457.     "A single vcard"
  458.  
  459.     def __init__(self, lines):
  460.         self._version = (2,0)  # which version of the vcard spec the card conforms to
  461.         self._origin = None    # which program exported the vcard
  462.         self._data = {}
  463.         self._groups = {}
  464.         self.lines = []
  465.         
  466.         # extract version field
  467.         for f,v in lines:
  468.             assert len(f)
  469.             
  470.             if f == ["X-EVOLUTION-FILE-AS"]: # all evolution cards have this
  471.                 self._origin = "evolution"
  472.             
  473.             if f[0].startswith("ITEM") and (f[0].endswith(".X-ABADR") or f[0].endswith(".X-ABLABEL")):
  474.                 self._origin = "apple"
  475.             
  476.             if len(v) and v[0].find(">!$_") > v[0].find("_$!<") >= 0:
  477.                 self.origin = "apple"
  478.             
  479.             if f == ["VERSION"]:
  480.                 ver = v.split(".")
  481.                 try:
  482.                     ver = [int(xx) for xx in ver]    
  483.                 except ValueError:
  484.                     raise VFileException(v+" is not a valid vcard version")
  485.                 
  486.                 self._version = ver
  487.                 continue
  488.             
  489.             # convert {home,work}.{tel,label} to {tel,label};{home,work}
  490.             # this probably dates from *very* early vcards
  491.             if f[0] == "HOME.TEL": 
  492.                 f[0:1] = ["TEL", "HOME"]
  493.                 
  494.             elif f[0] == "HOME.LABEL": 
  495.                 f[0:1] = ["LABEL", "HOME"]
  496.                 
  497.             elif f[0] == "WORK.TEL": 
  498.                 f[0:1] = ["TEL", "WORK"]
  499.                 
  500.             elif f[0] == "WORK.LABEL": 
  501.                 f[0:1] = ["LABEL", "WORK"]
  502.                 
  503.             self.lines.append( (f,v) )
  504.         
  505.         self._parse(self.lines, self._data)
  506.         self._update_groups(self._data)
  507.  
  508.         
  509.     def getdata(self):
  510.         "Returns a dict of the data parsed out of the vcard"
  511.         return self._data
  512.         
  513.     def get(self, key, default=''):
  514.         return self._data.get(key, default)
  515.  
  516.         
  517.     def _getfieldname(self, name, dict):
  518.         """Returns the fieldname to use in the dict.
  519.  
  520.         For example, if name is "email" and there is no "email" field
  521.         in dict, then "email" is returned.  If there is already an "email"
  522.         field then "email2" is returned, etc"""
  523.         if name not in dict:
  524.             return name
  525.         for i in xrange(2,99999):
  526.             if name+`i` not in dict:
  527.                 return name+`i`
  528.  
  529.                 
  530.     def _parse(self, lines, result):
  531.         for field,value in lines:
  532.             if len(value.strip()) == 0: # ignore blank values
  533.                 continue
  534.                 
  535.             if '.' in field[0]:
  536.                 f = field[0][field[0].find('.')+1:]
  537.             else: 
  538.                 f = field[0]
  539.             
  540.             t = f.replace("-", "_")
  541.             func = getattr(self, "_field_"+t, self._default_field)
  542.             func(field, value, result)
  543.  
  544.             
  545.     def _update_groups(self, result):
  546.         """Update the groups info """
  547.         for k,e in self._groups.items():
  548.             self._setvalue(result, *e)
  549.  
  550.             
  551.     # fields we ignore
  552.  
  553.     def _field_ignore(self, field, value, result):
  554.         pass
  555.  
  556.         
  557.     _field_LABEL = _field_ignore        # we use the ADR field instead
  558.     _field_BDAY = _field_ignore         # not stored in bitpim
  559.     _field_ROLE = _field_ignore         # not stored in bitpim
  560.     _field_CALURI = _field_ignore       # not stored in bitpim
  561.     _field_CALADRURI = _field_ignore    # variant of above
  562.     _field_FBURL = _field_ignore        # not stored in bitpim
  563.     _field_REV = _field_ignore          # not stored in bitpim
  564.     _field_KEY = _field_ignore          # not stored in bitpim
  565.     _field_SOURCE = _field_ignore       # not stored in bitpim (although arguably part of serials)
  566.     _field_PHOTO = _field_ignore        # contained either binary image, or external URL, not used by BitPim
  567.  
  568.     
  569.     # simple fields
  570.     
  571.     def _field_FN(self, field, value, result):
  572.         result[self._getfieldname("name", result)] = self.unquote(value)
  573.  
  574.         
  575.     def _field_TITLE(self, field, value, result):
  576.         result[self._getfieldname("title", result)] = self.unquote(value)
  577.  
  578.         
  579.     def _field_NICKNAME(self, field, value, result):
  580.         # ::TODO:: technically this is a comma seperated list ..
  581.         result[self._getfieldname("nickname", result)] = self.unquote(value)
  582.  
  583.         
  584.     def _field_NOTE(self, field, value, result):
  585.         result[self._getfieldname("notes", result)] = self.unquote(value)
  586.  
  587.         
  588.     def _field_UID(self, field, value, result):
  589.         result["uid"] = self.unquote(value) # note that we only store one UID (the "U" does stand for unique)
  590.  
  591.         
  592.     #
  593.     #  Complex fields
  594.     # 
  595.  
  596.     def _field_N(self, field, value, result):
  597.         value = self.splitandunquote(value)
  598.         familyname = givenname = additionalnames = honorificprefixes = honorificsuffixes = None
  599.         try:
  600.             familyname = value[0]
  601.             givenname = value[1]
  602.             additionalnames = value[2]
  603.             honorificprefixes = value[3]
  604.             honorificsuffixes = value[4]
  605.         except IndexError:
  606.             pass
  607.         
  608.         if familyname is not None and len(familyname):
  609.             result[self._getfieldname("last name", result)] = familyname
  610.         
  611.         if givenname is not None and len(givenname):
  612.             result[self._getfieldname("first name", result)] = givenname
  613.         
  614.         if additionalnames is not None and len(additionalnames):
  615.             result[self._getfieldname("middle name", result)] = additionalnames
  616.         
  617.         if honorificprefixes is not None and len(honorificprefixes):
  618.             result[self._getfieldname("prefix", result)] = honorificprefixes
  619.         
  620.         if honorificsuffixes is not None and len(honorificsuffixes):
  621.             result[self._getfieldname("suffix", result)] = honorificsuffixes
  622.  
  623.             
  624.     _field_NAME = _field_N  # early versions of vcard did this
  625.  
  626.     
  627.     def _field_ORG(self, field, value, result):
  628.         value = self.splitandunquote(value)
  629.         if len(value):
  630.             result[self._getfieldname("organisation", result)] = value[0]
  631.             
  632.         for f in value[1:]:
  633.             result[self._getfieldname("organisational unit", result)] = f
  634.  
  635.             
  636.     _field_O = _field_ORG # early versions of vcard did this
  637.  
  638.     
  639.     def _field_EMAIL(self, field, value, result):
  640.         value = self.unquote(value)
  641.         # work out the types
  642.         types = []
  643.         for f in field[1:]:
  644.             if f.startswith("TYPE="):
  645.                 ff = f[len("TYPE="):].split(",")
  646.             else: 
  647.                 ff = [f]
  648.                 
  649.             types.extend(ff)
  650.  
  651.         # the standard doesn't specify types of "home" and "work" but
  652.         # does allow for random user defined types, so we look for them
  653.         type = None
  654.         for t in types:
  655.             if t == "HOME": 
  656.                 type="home"
  657.                 
  658.             if t == "WORK": 
  659.                 type="business"
  660.                 
  661.             if t == "X400": 
  662.                 return # we don't want no steenking X.400
  663.  
  664.         preferred = "PREF" in types
  665.  
  666.         if type is None:
  667.             self._setvalue(result, "email", value, preferred)
  668.         else:
  669.             addr = {'email': value, 'type': type}
  670.             self._setvalue(result, "email", addr, preferred)
  671.  
  672.             
  673.     def _field_URL(self, field, value, result):
  674.         # the standard doesn't specify url types or a pref type,
  675.         # but we implement it anyway
  676.         value = self.unquote(value)
  677.         # work out the types
  678.         types = []
  679.         for f in field[1:]:
  680.             if f.startswith("TYPE="):
  681.                 ff = f[len("TYPE="):].split(",")
  682.             else: 
  683.                 ff=[f]
  684.                 
  685.             types.extend(ff)
  686.  
  687.         type = None
  688.         for t in types:
  689.             if t == "HOME": 
  690.                 type="home"
  691.                 
  692.             if t == "WORK": 
  693.                 type="business"
  694.  
  695.         preferred = "PREF" in types
  696.  
  697.         if type is None:    
  698.             self._setvalue(result, "url", value, preferred)
  699.         else:
  700.             addr = {'url': value, 'type': type}
  701.             self._setvalue(result, "url", addr, preferred)        
  702.  
  703.             
  704.     def _field_X_SPEEDDIAL(self, field, value, result):
  705.         if '.' in field[0]:
  706.             group = field[0][:field[0].find('.')]
  707.         else:
  708.             group = None
  709.         if group is None:
  710.             # this has to belong to a group!!
  711.             #print 'speedial has no group'
  712.             log.debug("speeddial has no group")
  713.         else:
  714.             self._setgroupvalue(result, 'phone', { 'speeddial': int(value) },
  715.                                 group, False)
  716.  
  717.                                 
  718.     def _field_TEL(self, field, value, result):
  719.         value = self.unquote(value)
  720.         # see if this is part of a group
  721.         if '.' in field[0]:
  722.             group = field[0][:field[0].find('.')]
  723.         else:
  724.             group = None
  725.  
  726.         # work out the types
  727.         types = []
  728.         
  729.         for f in field[1:]:
  730.             if f.startswith("TYPE="):
  731.                 ff = f[len("TYPE="):].split(",")
  732.             else: 
  733.                 ff = [f]
  734.                 
  735.             types.extend(ff)
  736.  
  737.         # type munging - we map vcard types to simpler ones
  738.         munge = { "BBS": "DATA", "MODEM": "DATA", "ISDN": "DATA", "CAR": "CELL", 
  739.             "PCS": "CELL" }
  740.             
  741.         types = [munge.get(t, t) for t in types]
  742.  
  743.         # reduce types to home, work, msg, pref, voice, fax, cell, video, pager, data
  744.         types = [t for t in types if t in ("HOME", "WORK", "MSG", "PREF", "VOICE", 
  745.             "FAX", "CELL", "VIDEO", "PAGER", "DATA")]
  746.  
  747.         # if type is in this list and voice not explicitly mentioned then it is not a voice type
  748.         antivoice = ["FAX", "PAGER", "DATA"]
  749.         
  750.         if "VOICE" in types:
  751.             voice = True
  752.         
  753.         else:
  754.             voice = True # default is voice
  755.             
  756.             for f in antivoice:
  757.                 if f in types:
  758.                     voice = False
  759.                     break
  760.                 
  761.         preferred = "PREF" in types
  762.  
  763.         # vcard allows numbers to be multiple things at the same time, such as home voice, home fax
  764.         # and work fax so we have to test for all variations
  765.  
  766.         # if neither work or home is specified, then no default (otherwise things get really complicated)
  767.         iswork = False
  768.         ishome = False
  769.         if "WORK" in types: 
  770.             iswork = True
  771.             
  772.         if "HOME" in types: 
  773.             ishome = True
  774.  
  775.         if len(types) == 0 or types == ["PREF"]: 
  776.             iswork = True # special case when nothing else is specified
  777.     
  778.         
  779.         value = phonenumber_normalise(value)
  780.         if iswork and voice:
  781.             self._setgroupvalue(result,
  782.                            "phone", {"type": "business", "number": value},
  783.                            group, preferred)
  784.                            
  785.         if ishome and voice:
  786.             self._setgroupvalue(result,
  787.                            "phone", {"type": "home", "number": value},
  788.                            group, preferred)
  789.                            
  790.         if not iswork and not ishome and "FAX" in types:
  791.             # fax without explicit work or home
  792.             self._setgroupvalue(result,
  793.                            "phone", {"type": "fax", "number": value},
  794.                            group, preferred)
  795.                            
  796.         else:
  797.             if iswork and "FAX" in types:
  798.                 self._setgroupvalue(result, "phone",
  799.                                {"type": "business fax", "number": value},
  800.                                group, preferred)
  801.             
  802.             if ishome and "FAX" in types:
  803.                 self._setgroupvalue(result, "phone",
  804.                                {"type": "home fax", "number": value},
  805.                                group, preferred)
  806.         
  807.         if "CELL" in types:
  808.             self._setgroupvalue(result,
  809.                            "phone", {"type": "cell", "number": value},
  810.                            group, preferred)
  811.         
  812.         if "PAGER" in types:
  813.             self._setgroupvalue(result,
  814.                            "phone", {"type": "pager", "number": value},
  815.                            group, preferred)
  816.         
  817.         if "DATA" in types:
  818.             self._setgroupvalue(result,
  819.                            "phone", {"type": "data", "number": value},
  820.                            group, preferred)
  821.  
  822.                            
  823.     def _setgroupvalue(self, result, type, value, group, preferred=False):
  824.         """ Set value of an item of a group
  825.         """
  826.         if group is None:
  827.             # no groups specified
  828.             return self._setvalue(result, type, value, preferred)
  829.             
  830.         group_type = self._groups.get(group, None)
  831.         
  832.         if group_type is None:
  833.             # 1st one of the group
  834.             self._groups[group] = [type, value, preferred]
  835.         
  836.         else:
  837.             if type != group_type[0]:
  838.                 log.debug('Group %s has different types: %s, %s' % (group, type,groups_type[0]))
  839.             
  840.             if preferred:
  841.                 group_type[2] = True
  842.             
  843.             group_type[1].update(value)
  844.  
  845.             
  846.     def _setvalue(self, result, type, value, preferred=False):
  847.         if type not in result:
  848.             result[type] = value
  849.             return
  850.             
  851.         if not preferred:
  852.             result[self._getfieldname(type, result)] = value
  853.             return
  854.             
  855.         # we need to insert our value at the begining
  856.         values = [value]
  857.         
  858.         for suffix in [""]+range(2,99):
  859.             if type+str(suffix) in result:
  860.                 values.append(result[type+str(suffix)])
  861.             else:
  862.                 break
  863.                 
  864.         suffixes = [""]+range(2,len(values)+1)
  865.         
  866.         for l in range(len(suffixes)):
  867.             result[type+str(suffixes[l])] = values[l]
  868.  
  869.             
  870.     def _field_CATEGORIES(self, field, value, result):
  871.         # comma seperated just for fun
  872.         values = self.splitandunquote(value, seperator=",")
  873.         values = [v.replace(";", "").strip() for v in values]  # semi colon is used as seperator in bitpim text field
  874.         values = [v for v in values if len(v)]
  875.         v = result.get('categories', None)
  876.         
  877.         if v:
  878.             result['categories'] = ';'.join([v, ";".join(values)])
  879.         
  880.         else:
  881.             result['categories'] = ';'.join(values)
  882.  
  883.             
  884.     def _field_SOUND(self, field, value, result):
  885.         # comma seperated just for fun
  886.         values = self.splitandunquote(value, seperator=",")
  887.         values = [v.replace(";", "").strip() for v in values]  # semi colon is used as seperator in bitpim text field
  888.         values = [v for v in values if len(v)]
  889.         result[self._getfieldname("ringtones", result)] = ";".join(values)
  890.         
  891.         
  892.     _field_CATEGORY = _field_CATEGORIES  # apple use "category" which is not in the spec
  893.  
  894.     
  895.     def _field_ADR(self, field, value, result):
  896.         # work out the type
  897.         preferred = False
  898.         type = "business"
  899.         
  900.         for f in field[1:]:
  901.             if f.startswith("TYPE="):
  902.                 ff = f[len("TYPE="):].split(",")
  903.                 
  904.             else: 
  905.                 ff = [f]
  906.                 
  907.             for x in ff:
  908.                 if x == "HOME":
  909.                     type = "home"
  910.                 if x == "PREF":
  911.                     preferred = True
  912.         
  913.         value = self.splitandunquote(value)
  914.         pobox = extendedaddress = streetaddress = locality = region = postalcode = country = None
  915.         try:
  916.             pobox = value[0]
  917.             extendedaddress = value[1]
  918.             streetaddress = value[2]
  919.             locality = value[3]
  920.             region = value[4]
  921.             postalcode = value[5]
  922.             country = value[6]
  923.         except IndexError:
  924.             pass
  925.         
  926.         addr = {}
  927.         
  928.         if pobox is not None and len(pobox):
  929.             addr["pobox"] = pobox
  930.         
  931.         if extendedaddress is not None and len(extendedaddress):
  932.             addr["street2"] = extendedaddress
  933.         
  934.         if streetaddress is not None and len(streetaddress):
  935.             addr["street"] = streetaddress
  936.         
  937.         if locality is not None and len(locality):
  938.             addr["city"] = locality
  939.         
  940.         if region is not None and len(region):
  941.             addr["state"] = region
  942.         
  943.         if postalcode is not None and len(postalcode):
  944.             addr["postalcode"] = postalcode
  945.         
  946.         if country is not None and len(country):
  947.             addr["country"] = country
  948.         
  949.         if len(addr):
  950.             addr["type"] = type
  951.             self._setvalue(result, "address", addr, preferred)
  952.  
  953.             
  954.     def _field_X_PALM(self, field, value, result):
  955.         # handle a few PALM custom fields
  956.         ff = field[0].split(".")
  957.         f0 = ff[0]
  958.         
  959.         if len(ff) > 1:
  960.             f1 = ff[1]
  961.         else:
  962.             f1 = ''
  963.             
  964.         if f0.startswith('X-PALM-CATEGORY') or f1.startswith('X-PALM-CATEGORY'):
  965.             self._field_CATEGORIES(['CATEGORIES'], value, result)
  966.         
  967.         elif f0 == 'X-PALM-NICKNAME' or f1 == 'X-PALM-NICKNAME':
  968.             self._field_NICKNAME(['NICKNAME'], value, result)
  969.         
  970.         else:
  971.             log.debug("Ignoring PALM custom field: %s" % field)
  972.         
  973.         
  974.     def _default_field(self, field, value, result):
  975.         ff = field[0].split(".")
  976.         f0 = ff[0]
  977.         
  978.         if len(ff) > 1:
  979.             f1 = ff[1]
  980.         else:
  981.             f1 = ''
  982.         
  983.         if f0.startswith('X-PALM-') or f1.startswith('X-PALM-'):
  984.             self._field_X_PALM(field, value, result)
  985.             return
  986.         
  987.         elif f0.startswith("X-") or f1.startswith("X-"):
  988.             log.debug("Ignoring custom field: %s" % field)
  989.             return
  990.         
  991.         log.debug("No idea what to do with %s (%s)" % (field, value[:80]))
  992.         
  993.  
  994.             
  995.     def unquote(self, value):
  996.         # ::TODO:: do this properly (deal with all backslashes)
  997.         return value.replace(r"\;", ";") \
  998.                .replace(r"\,", ",") \
  999.                .replace(r"\n", "\n") \
  1000.                .replace(r"\r\n", "\r\n") \
  1001.                .replace("\r\n", "\n") \
  1002.                .replace("\r", "\n")
  1003.  
  1004.                
  1005.     def splitandunquote(self, value, seperator=";"):
  1006.         # also need a splitandsplitandunquote since some ; delimited fields are then comma delimited
  1007.  
  1008.         # short cut for normal case - no quoted seperators
  1009.         if value.find("\\"+seperator)<0:
  1010.             return [self.unquote(v) for v in value.split(seperator)]
  1011.  
  1012.         # funky quoting, do it the slow hard way
  1013.         res = []
  1014.         build = ""
  1015.         v = 0
  1016.         while v < len(value):
  1017.             if value[v] == seperator:
  1018.                 res.append(build)
  1019.                 build = ""
  1020.                 v += 1
  1021.                 continue    
  1022.         
  1023.  
  1024.             if value[v] == "\\":
  1025.                 build += value[v:v+2]
  1026.                 v += 2
  1027.                 continue
  1028.                 
  1029.             build += value[v]
  1030.             v += 1
  1031.         
  1032.         if len(build):
  1033.             res.append(build)
  1034.  
  1035.         return [self.unquote(v) for v in res]
  1036.  
  1037.         
  1038.     def version(self):
  1039.         "Best guess as to vcard version"
  1040.         return self._version
  1041.  
  1042.         
  1043.     def origin(self):
  1044.         "Best guess as to what program wrote the vcard"
  1045.         return self._origin
  1046.         
  1047.         
  1048.     def __getitem__(self, item):
  1049.         return self._data[item]
  1050.         
  1051.     def __repr__(self):
  1052.         return repr(self._data)
  1053.  
  1054.         
  1055. # The formatters return a string
  1056. def myqpencodestring(value):
  1057.     """My own routine to do qouted printable since the builtin one doesn't encode CR or NL!"""
  1058.     return quopri.encodestring(value).replace("\r", "=0D").replace("\n", "=0A")
  1059.  
  1060.     
  1061. def format_stringv2(value):
  1062.     """Return a vCard v2 string.  Any embedded commas or semi-colons are removed."""
  1063.     return value.replace("\\", "").replace(",", "").replace(";", "")
  1064.  
  1065.     
  1066. def format_stringv3(value):
  1067.     """Return a vCard v3 string.  Embedded commas and semi-colons are backslash quoted"""
  1068.     return value.replace("\\", "").replace(",", r"\,").replace(";", r"\;")
  1069.  
  1070.     
  1071. _string_formatters = (format_stringv2, format_stringv3)
  1072.  
  1073.  
  1074. def format_binary(value):
  1075.     """Return base 64 encoded string"""
  1076.     # encodestring always adds a newline so we have to strip it off
  1077.     return base64.encodestring(value).rstrip()
  1078.  
  1079.     
  1080. def _is_sequence(v):
  1081.     """Determine if v is a sequence such as passed to value in out_line.
  1082.     Note that a sequence of chars is not a sequence for our purposes."""
  1083.     return isinstance(v, (type( () ), type([])))
  1084.  
  1085.     
  1086. def out_line(name, attributes, value, formatter, join_char=";"):
  1087.     """Returns a single field correctly formatted and encoded (including trailing newline)
  1088.  
  1089.     @param name:  The field name
  1090.     @param attributes: A list of string attributes (eg "TYPE=intl,post" ).  Usually
  1091.                   empty except for TEL and ADR.  You can also pass in None.
  1092.     @param value: The field value.  You can also pass in a list of components which will be
  1093.                   joined with join_char such as the 6 components of N
  1094.     @param formatter:  The function that formats the value/components.  See the
  1095.                   various format_ functions.  They will automatically ensure that
  1096.                   ENCODING=foo attributes are added if appropriate"""
  1097.  
  1098.     if attributes is None:     
  1099.         attributes = [] # ensure it is a list
  1100.     else: 
  1101.         attributes = list(attributes[:]) # ensure we work with a copy
  1102.  
  1103.     if formatter in _string_formatters:
  1104.         if _is_sequence(value):
  1105.             qp = False
  1106.             for f in value:
  1107.                 f = formatter(f)
  1108.                 if myqpencodestring(f) != f:
  1109.                     qp = True
  1110.                     break
  1111.             
  1112.             if qp:
  1113.                 attributes.append("ENCODING=QUOTED-PRINTABLE")
  1114.                 value = [myqpencodestring(f) for f in value]
  1115.                 
  1116.             value = join_char.join(value)
  1117.         else:
  1118.             value = formatter(value)
  1119.             # do the qp test
  1120.             qp = myqpencodestring(value) != value
  1121.             if qp:
  1122.                 value = myqpencodestring(value)
  1123.                 attributes.append("ENCODING=QUOTED-PRINTABLE")
  1124.     else:
  1125.         assert not _is_sequence(value)
  1126.         if formatter is not None:
  1127.             value = formatter(value) # ::TODO:: deal with binary and other formatters and their encoding types
  1128.  
  1129.     res = ";".join([name]+attributes)+":"
  1130.     res += _line_reformat(value, 70, 70-len(res))
  1131.     assert res[-1] != "\n"
  1132.     
  1133.     return res+"\n"
  1134.  
  1135.     
  1136. def _line_reformat(line, width=70, firstlinewidth=0):
  1137.     """Takes line string and inserts newlines
  1138.     and spaces on following continuation lines
  1139.     so it all fits in width characters
  1140.  
  1141.     @param width: how many characters to fit it in
  1142.     @param firstlinewidth: if >0 then first line is this width.
  1143.          if equal to zero then first line is same width as rest.
  1144.          if <0 then first line will go immediately to continuation.
  1145.          """
  1146.     if firstlinewidth == 0: 
  1147.         firstlinewidth = width
  1148.     
  1149.     if len(line) < firstlinewidth:
  1150.         return line
  1151.     
  1152.     res = ""
  1153.     
  1154.     if firstlinewidth > 0:
  1155.         res += line[:firstlinewidth]
  1156.         line = line[firstlinewidth:]
  1157.     
  1158.     while len(line):
  1159.         res += "\n "+line[:width]
  1160.         if len(line)<width: 
  1161.             break
  1162.             
  1163.         line = line[width:]
  1164.     
  1165.     return res
  1166.  
  1167. def out_names(vals, formatter, limit=1):
  1168.     res = ""
  1169.     for v in vals[:limit]:
  1170.         # full name
  1171.         res += out_line("FN", None, nameparser_formatsimplename(v), formatter)
  1172.         # name parts
  1173.         f,m,l = nameparser_getparts(v)
  1174.         res += out_line("N", None, (l,f,m,"",""), formatter)
  1175.         # nickname
  1176.         nn = v.get("nickname", "")
  1177.         
  1178.         if len(nn):
  1179.             res += out_line("NICKNAME", None, nn, formatter)
  1180.     
  1181.     return res
  1182.  
  1183. # Apple uses wrong field name so we do some futzing ...
  1184. def out_categories(vals, formatter, field="CATEGORIES"):
  1185.     cats = [v.get("category") for v in vals]
  1186.     if len(cats):
  1187.         return out_line(field, None, cats, formatter, join_char=",")
  1188.     
  1189.     return ""
  1190.  
  1191.     
  1192. def out_categories_apple(vals, formatter):
  1193.     return out_categories(vals, formatter, field="CATEGORY")
  1194.  
  1195.     
  1196. # Used for both email and urls. we don't put any limits on how many are output
  1197. def out_eu(vals, formatter, field, bpkey):
  1198.     res = ""
  1199.     first = True
  1200.     for v in vals:
  1201.         val = v.get(bpkey)
  1202.         type = v.get("type", "")
  1203.         
  1204.         if len(type):
  1205.             if type == "business": 
  1206.                 type = "work" # vcard uses different name
  1207.             
  1208.             type = type.upper()
  1209.             
  1210.             if first:
  1211.                 type = type+",PREF"
  1212.         
  1213.         elif first:
  1214.             type = "PREF"
  1215.         
  1216.         if len(type):
  1217.             type = ["TYPE="+type+["",",INTERNET"][field == "EMAIL"]] # email also has "INTERNET"
  1218.         else:
  1219.             type = None
  1220.             
  1221.         res += out_line(field, type, val, formatter)
  1222.         first = False
  1223.     
  1224.     return res
  1225.  
  1226.     
  1227. def out_emails(vals, formatter):
  1228.     return out_eu(vals, formatter, "EMAIL", "email")
  1229.  
  1230.     
  1231. def out_urls(vals, formatter):
  1232.     return out_eu(vals, formatter, "URL", "url")
  1233.  
  1234. _out_tel_mapping = { 
  1235. 'home': 'HOME',
  1236. 'office': 'WORK',
  1237. 'cell': 'CELL',
  1238. 'fax': 'FAX',
  1239. 'pager': 'PAGER',
  1240. 'data': 'MODEM',
  1241. 'none': 'VOICE'
  1242. }
  1243.                    
  1244.                    
  1245. def out_tel(vals, formatter):
  1246.     # ::TODO:: limit to one type of each number
  1247.     phones = ['phone'+str(x) for x in ['']+range(2,len(vals)+1)]
  1248.     res = ""
  1249.     first = True
  1250.     idx = 0
  1251.     
  1252.     for v in vals:
  1253.         sp = v.get('speeddial', None)
  1254.         
  1255.         if sp is None:
  1256.             # no speed dial
  1257.             res += out_line("TEL",
  1258.                           ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
  1259.                           phonenumber_format(v['number']), formatter)
  1260.         else:
  1261.             res += out_line(phones[idx]+".TEL",
  1262.                           ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
  1263.                           phonenumber_format(v['number']), formatter)
  1264.             res += out_line(phones[idx]+".X-SPEEDDIAL", None, str(sp), formatter)
  1265.             idx += 1
  1266.         first = False
  1267.     
  1268.     return res
  1269.  
  1270.     
  1271. # and addresses
  1272. def out_adr(vals, formatter):
  1273.     # ::TODO:: limit to one type of each address, and only one org
  1274.     res = ""
  1275.     first = True
  1276.     for v in vals:
  1277.         o = v.get("company", "")
  1278.         
  1279.         if len(o):
  1280.             res += out_line("ORG", None, o, formatter)
  1281.         
  1282.         if v.get("type") == "home": 
  1283.             type = "HOME"
  1284.         else: 
  1285.             type = "WORK"
  1286.             
  1287.         type = "TYPE="+type+("", ",PREF")[first]
  1288.         res += out_line("ADR", [type], [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], formatter)
  1289.         first = False
  1290.     
  1291.     return res
  1292.  
  1293.     
  1294. def out_note(vals, formatter, limit=1):
  1295.     return "".join([out_line("NOTE", None, v["memo"], formatter) for v in vals[:limit]])
  1296.  
  1297.     
  1298. # Sany SCP-6600 (Katana) support
  1299. def out_tel_scp6600(vals, formatter):
  1300.     res = ""
  1301.     _pref = len(vals) > 1
  1302.     
  1303.     if _pref:
  1304.         s = "PREF,"
  1305.     else:
  1306.         s = ''
  1307.     
  1308.     for v in vals:
  1309.         res += out_line("TEL", s,
  1310.                       ["TYPE=%s%s" % (s, _out_tel_mapping[v['type']])],
  1311.                       phonenumber_format(v['number']), formatter)
  1312.         
  1313.         _pref = False
  1314.         
  1315.     return res
  1316.     
  1317.     
  1318. def out_email_scp6600(vals, formatter):
  1319.     res = ''
  1320.     for _idx in range(min(len(vals), 2)):
  1321.         v = vals[_idx]
  1322.         
  1323.         if v.get('email', None):
  1324.             res += out_line('EMAIL', ['TYPE=INTERNET'],
  1325.                           v['email'], formatter)
  1326.     
  1327.     return res
  1328.     
  1329.     
  1330. def out_url_scp660(vals, formatter):
  1331.     if vals and vals[0].get('url', None):
  1332.         return out_line('URL', None, vals[0]['url'], formatter)
  1333.     return ''
  1334.     
  1335.     
  1336. def out_adr_scp6600(vals, formatter):
  1337.     for v in vals:
  1338.         if v.get('type', None) == 'home':
  1339.             _type = 'HOME'
  1340.         else:
  1341.             _type = 'WORK'
  1342.         return out_line("ADR", ['TYPE=%s' % _type],
  1343.                         [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")],
  1344.                         formatter)
  1345.     return ''
  1346.  
  1347.     
  1348. # This is the order we write things out to the vcard.  Although
  1349. # vCard doesn't require an ordering, it looks nicer if it
  1350. # is (eg name first)
  1351. _field_order = ("names", "wallpapers", "addresses", "numbers", "categories", 
  1352.                 "emails", "urls", "ringtones", "flags", "memos", "serials")
  1353.  
  1354.  
  1355. def output_entry(entry, profile, limit_fields=None):
  1356.  
  1357.     # debug build assertion that limit_fields only contains fields we know about
  1358.     if __debug__ and limit_fields is not None:
  1359.         assert len([f for f in limit_fields if f not in _field_order]) == 0
  1360.     
  1361.     fmt = profile["_formatter"]
  1362.     io = cStringIO.StringIO()
  1363.     io.write(out_line("BEGIN", None, "VCARD", None))
  1364.     io.write(out_line("VERSION", None, profile["_version"], None))
  1365.  
  1366.     if limit_fields is None:
  1367.         fields = _field_order
  1368.     else:
  1369.         fields = [f for f in _field_order if f in limit_fields]
  1370.  
  1371.     for f in fields:
  1372.         if f in entry and f in profile:
  1373.             func = profile[f]
  1374.             # does it have a limit?  (nice scary introspection :-)
  1375.             if "limit" in func.func_code.co_varnames[:func.func_code.co_argcount]:
  1376.                 lines = func(entry[f], fmt, limit = profile["_limit"])
  1377.             else:
  1378.                 lines = func(entry[f], fmt)
  1379.             if len(lines):
  1380.                 io.write(lines)
  1381.  
  1382.     io.write(out_line("END", None, "VCARD", fmt))
  1383.     return io.getvalue()
  1384.  
  1385.     
  1386. profile_vcard2 = {
  1387. '_formatter': format_stringv2,
  1388. '_limit': 1,
  1389. '_version': "2.1",
  1390. 'names': out_names,
  1391. 'categories': out_categories,
  1392. 'emails': out_emails,
  1393. 'urls': out_urls,
  1394. 'numbers': out_tel,
  1395. 'addresses': out_adr,
  1396. 'memos': out_note,
  1397.     }
  1398.  
  1399. profile_vcard3 = profile_vcard2.copy()
  1400. profile_vcard3['_formatter'] = format_stringv3
  1401. profile_vcard3['_version'] = "3.0"
  1402.  
  1403. profile_apple = profile_vcard3.copy()
  1404. profile_apple['categories'] = out_categories_apple
  1405.  
  1406. profile_full = profile_vcard3.copy()
  1407. profile_full['_limit'] = 99999
  1408.  
  1409. profile_scp6600 = profile_full.copy()
  1410. del profile_scp6600['categories']
  1411.  
  1412. profile_scp6600.update(
  1413. { 'numbers': out_tel_scp6600,
  1414.   'emails': out_email_scp6600,
  1415.   'urls': out_url_scp660,
  1416.   'addresses': out_adr_scp6600,
  1417.   })
  1418.  
  1419. profiles = {
  1420. 'vcard2':  { 'description': "vCard v2.1", 'profile': profile_vcard2 },
  1421. 'vcard3':  { 'description': "vCard v3.0", 'profile': profile_vcard3 },
  1422. 'apple':   { 'description': "Apple",      'profile': profile_apple  },
  1423. 'fullv3':  { 'description': "Full vCard v3.0", 'profile': profile_full},
  1424. 'scp6600': { 'description': "Sanyo SCP-6600 (Katana)",
  1425.              'profile': profile_scp6600 },
  1426. }
  1427.  
  1428.  
  1429.  
  1430.